spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { Bars } from '@/components/charts/bars';5import { LineChart } from '@/components/charts/line-chart';6import { Sparkline } from '@/components/charts/sparkline';7import { CompanyTable } from '@/components/company/company-table';8import { EventList } from '@/components/events/event-row';9import { EventTypeBadge } from '@/components/ui/badges';10import { Container, Empty, Note, PageHeader, Section, Stat, StatGrid } from '@/components/ui/section';11import { api, ApiError, safe } from '@/lib/api';12import { countryName } from '@/lib/countries';13import { fmtInt, fmtPctSigned, fmtScore } from '@/lib/format';14import { routes } from '@/lib/site';15import type { CompanyCard, IndustryDetailRaw } from '@/lib/types';1617export const revalidate = 300;1819async function load(slug: string): Promise<IndustryDetailRaw> {20 try {21 return await api.industry(slug);22 } catch (e) {23 if (e instanceof ApiError && e.notFound) notFound();24 throw e;25 }26}27export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {28 const { slug } = await params;29 const d = await safe(api.industry(slug));30 if (!d) return { title: 'Industry' };31 return { title: `${d.name} — industry atlas`, description: `${d.name}: ${Array.isArray(d.companies) ? d.companies.length : d.companies} monitored companies, ${d.events_30d} structured events in 30 days, hiring momentum ${fmtPctSigned(d.hiring_momentum_30d)}.`, alternates: { canonical: `/industry/${slug}` } };32}3334export default async function IndustryPage({ params }: { params: Promise<{ slug: string }> }) {35 const { slug } = await params;36 const d = await load(slug);37 const companies: CompanyCard[] = Array.isArray(d.companies) ? d.companies : [];38 const count = Array.isArray(d.companies) ? d.companies.length : d.companies;39 const countries = d.countries ?? [];40 return (41 <Container wide>42 <PageHeader43 eyebrow={44 <>45 <Link href={routes.industries()} className="hover:text-ink">46 Industry atlas47 </Link>48 {d.parent_slug && (49 <>50 <span>/</span>51 <Link href={routes.industry(d.parent_slug)} className="hover:text-ink">52 {d.parent_slug.replace(/-/g, ' ')}53 </Link>54 </>55 )}56 </>57 }58 title={d.name}59 lede={d.description ?? `Monitored companies classified under ${d.name}.`}60 />61 <StatGrid cols={6}>62 <Stat label="Companies" value={fmtInt(count)} size="sm" />63 <Stat label="Events 7 d" value={fmtInt(d.events_7d)} size="sm" />64 <Stat label="Events 30 d" value={fmtInt(d.events_30d)} size="sm" />65 <Stat label="Activity" value={fmtScore(d.activity_score)} size="sm" />66 <Stat label="Hiring 30 d" value={fmtPctSigned(d.hiring_momentum_30d)} size="sm" delta={d.hiring ? { value: `${fmtInt(d.hiring.open)} open`, tone: 'neutral' } : undefined} />67 <Stat label="AI adoption" value={fmtScore(d.ai_adoption)} size="sm" />68 </StatGrid>69 <div className="grid gap-8 lg:grid-cols-12">70 <Section eyebrow="Activity" title="Industry activity, 90 days" className="lg:col-span-8">71 {d.series?.length > 1 ? <LineChart series={[{ id: 'activity', label: 'Activity score', points: d.series.map((p) => ({ day: p.day, value: p.value })) }]} height={200} yZero /> : <Empty compact title="Not enough history yet." />}72 </Section>73 <Section eyebrow="Composition" title="Countries" className="lg:col-span-4">74 {countries.length ? <Bars dense rows={countries.slice(0, 10).map((c) => ({ key: c.country, label: countryName(c.country), value: c.companies, href: routes.country(c.country) }))} /> : <Empty compact />}75 {d.top_event_types?.length > 0 && (76 <p className="mt-4 flex flex-wrap items-center gap-1 text-xs text-ink-3">77 Top event types:78 {d.top_event_types.map((t) => (79 <EventTypeBadge key={t} type={t} small />80 ))}81 </p>82 )}83 </Section>84 </div>85 <Section eyebrow="Companies" title="Most active monitored companies" action={{ href: routes.companies({ industry: slug }), label: 'All in directory' }}>86 <CompanyTable items={companies} />87 </Section>88 <div className="grid gap-8 lg:grid-cols-12">89 <Section eyebrow="Events" title="Latest structured events" action={{ href: routes.events({ industry: slug }), label: 'All events' }} className="lg:col-span-8">90 <EventList events={d.events ?? []} variant="table" />91 </Section>92 <Section eyebrow="Trending" title="Terms gaining momentum" className="lg:col-span-4">93 {d.trending?.length ? (94 <ul className="divide-y divide-rule border-y border-rule text-sm">95 {d.trending.slice(0, 8).map((t) => (96 <li key={t.term} className="flex items-center gap-2 py-1.5">97 <span className="min-w-0 flex-1 truncate">{t.term}</span>98 <Sparkline values={t.series} width={48} height={14} />99 <span className={`tnum text-xs ${(t.momentum ?? 0) > 0 ? 'text-positive' : 'text-ink-3'}`}>{fmtPctSigned(t.momentum, 0)}</span>100 </li>101 ))}102 </ul>103 ) : (104 <Empty compact />105 )}106 <Note className="mt-3">Hiring: {d.hiring ? `${fmtInt(d.hiring.new_30d)} new and ${fmtInt(d.hiring.removed_30d)} no-longer-listed roles in 30 days across monitored careers pages.` : 'summary unavailable.'}</Note>107 </Section>108 </div>109 </Container>110 );111}112